summaryrefslogtreecommitdiff
path: root/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-domain-assignment-dialog.tsx
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-07-23 06:06:27 +0000
committerjoonhoekim <26rote@gmail.com>2025-07-23 06:06:27 +0000
commitf9bfc82880212e1a13f6bbb28ecfc87b89346f26 (patch)
treeee792f340ebfa7eaf30d2e79f99f41213e5c5cf3 /app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-domain-assignment-dialog.tsx
parentedc0eabc8f5fc44408c28023ca155bd73ddf8183 (diff)
(김준회) 메뉴접근제어(부서별) 메뉴 구현
Diffstat (limited to 'app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-domain-assignment-dialog.tsx')
-rw-r--r--app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-domain-assignment-dialog.tsx384
1 files changed, 384 insertions, 0 deletions
diff --git a/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-domain-assignment-dialog.tsx b/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-domain-assignment-dialog.tsx
new file mode 100644
index 00000000..277511cb
--- /dev/null
+++ b/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-domain-assignment-dialog.tsx
@@ -0,0 +1,384 @@
+"use client";
+
+import * as React from "react";
+import { Loader2, Users, Building2, AlertCircle } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Textarea } from "@/components/ui/textarea";
+import { Badge } from "@/components/ui/badge";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { Label } from "@/components/ui/label";
+import { Separator } from "@/components/ui/separator";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import {
+ DepartmentNode
+} from "@/lib/users/knox-service";
+import {
+ getDepartmentDomainAssignmentsByDepartments,
+ type UserDomain
+} from "@/lib/users/department-domain/service";
+import { DOMAIN_OPTIONS, getDomainLabel } from "./domain-constants";
+
+interface ExistingAssignment {
+ id: number;
+ companyCode: string;
+ departmentCode: string;
+ departmentName: string;
+ assignedDomain: string;
+ description?: string | null;
+ createdAt: Date;
+ updatedAt: Date;
+}
+
+interface DepartmentDomainAssignmentDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ selectedDepartments: string[];
+ departments: DepartmentNode[];
+ companyInfo: { code: string; name: string };
+ onAssign: (assignments: {
+ departmentCodes: string[];
+ domain: string;
+ description?: string;
+ }) => Promise<void>;
+ isLoading?: boolean;
+}
+
+export function DepartmentDomainAssignmentDialog({
+ open,
+ onOpenChange,
+ selectedDepartments,
+ departments,
+ companyInfo,
+ onAssign,
+ isLoading = false,
+}: DepartmentDomainAssignmentDialogProps) {
+ const [selectedDomain, setSelectedDomain] = React.useState<string>("");
+ const [description, setDescription] = React.useState<string>("");
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
+ const [existingAssignments, setExistingAssignments] = React.useState<ExistingAssignment[]>([]);
+ const [isLoadingAssignments, setIsLoadingAssignments] = React.useState(false);
+
+ // 선택된 부서들의 정보 가져오기
+ const getSelectedDepartmentInfo = React.useCallback(() => {
+ const findDepartment = (nodes: DepartmentNode[], code: string): DepartmentNode | null => {
+ for (const node of nodes) {
+ if (node.departmentCode === code) {
+ return node;
+ }
+ const found = findDepartment(node.children, code);
+ if (found) return found;
+ }
+ return null;
+ };
+
+ return selectedDepartments
+ .map(code => findDepartment(departments, code))
+ .filter(Boolean) as DepartmentNode[];
+ }, [departments, selectedDepartments]);
+
+ // 회사별로 그룹화
+ const selectedDepartmentsByCompany = React.useMemo(() => {
+ const deptInfo = getSelectedDepartmentInfo();
+ const grouped = new Map<string, DepartmentNode[]>();
+
+ deptInfo.forEach(dept => {
+ if (!grouped.has(dept.companyCode)) {
+ grouped.set(dept.companyCode, []);
+ }
+ grouped.get(dept.companyCode)!.push(dept);
+ });
+
+ return grouped;
+ }, [getSelectedDepartmentInfo]);
+
+ // 기존 할당 정보 조회
+ React.useEffect(() => {
+ if (open && selectedDepartments.length > 0) {
+ const loadExistingAssignments = async () => {
+ setIsLoadingAssignments(true);
+ try {
+ const assignments = await getDepartmentDomainAssignmentsByDepartments(selectedDepartments);
+ setExistingAssignments(assignments as ExistingAssignment[]);
+ } catch (error) {
+ console.error("기존 할당 정보 조회 실패:", error);
+ setExistingAssignments([]);
+ } finally {
+ setIsLoadingAssignments(false);
+ }
+ };
+
+ loadExistingAssignments();
+ } else {
+ setExistingAssignments([]);
+ }
+ }, [open, selectedDepartments]);
+
+ // 폼 초기화
+ React.useEffect(() => {
+ if (open) {
+ setSelectedDomain("");
+ setDescription("");
+ setIsSubmitting(false);
+ }
+ }, [open]);
+
+ // 할당 처리
+ const handleAssign = async () => {
+ if (!selectedDomain || selectedDepartments.length === 0) {
+ return;
+ }
+
+ setIsSubmitting(true);
+
+ try {
+ await onAssign({
+ departmentCodes: selectedDepartments,
+ domain: selectedDomain,
+ description: description.trim() || undefined,
+ });
+
+ // 성공 시 다이얼로그 닫기
+ onOpenChange(false);
+ } catch (error) {
+ console.error("도메인 할당 실패:", error);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const canSubmit = selectedDomain && selectedDepartments.length > 0 && !isSubmitting && !isLoading;
+ const selectedDomainInfo = DOMAIN_OPTIONS.find(opt => opt.value === selectedDomain);
+ const hasConflicts = existingAssignments.some(a => a.assignedDomain !== selectedDomain && selectedDomain);
+
+ return (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden flex flex-col">
+ <DialogHeader>
+ <DialogTitle className="flex items-center gap-2">
+ <Building2 className="h-5 w-5" />
+ 부서별 도메인 할당
+ </DialogTitle>
+ <DialogDescription>
+ 선택된 {selectedDepartments.length}개 부서에 도메인을 할당합니다.
+ 상위 부서를 선택한 경우 하위 부서들도 자동으로 포함됩니다.
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="flex-1 overflow-hidden">
+ <ScrollArea className="h-full pr-4">
+ <div className="space-y-6">
+ {/* 선택된 부서들 표시 */}
+ <div className="space-y-3">
+ <Label className="text-sm font-medium flex items-center gap-2">
+ <Users className="h-4 w-4" />
+ 선택된 부서 ({selectedDepartments.length}개)
+ </Label>
+
+ <div className="border rounded-md p-3 max-h-32 overflow-y-auto">
+ {Array.from(selectedDepartmentsByCompany.entries()).map(([companyCode, depts]) => (
+ <div key={companyCode} className="mb-3 last:mb-0">
+ <div className="text-sm font-medium text-muted-foreground mb-2">
+ {companyCode} - {companyInfo.name}
+ </div>
+ <div className="flex flex-wrap gap-2">
+ {depts.map((dept) => (
+ <Badge
+ key={dept.departmentCode}
+ variant="outline"
+ className="text-xs"
+ >
+ {dept.departmentName || dept.departmentCode}
+ </Badge>
+ ))}
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+
+ {/* 기존 할당 현황 */}
+ {(existingAssignments.length > 0 || isLoadingAssignments) && (
+ <>
+ <Separator />
+ <div className="space-y-3">
+ <Label className="text-sm font-medium flex items-center gap-2">
+ <AlertCircle className="h-4 w-4" />
+ 현재 할당 현황
+ </Label>
+
+ {isLoadingAssignments ? (
+ <div className="flex items-center justify-center py-4">
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
+ 기존 할당 정보를 조회하는 중...
+ </div>
+ ) : (
+ <div className="border rounded-md">
+ <Table>
+ <TableHeader>
+ <TableRow>
+ <TableHead>부서</TableHead>
+ <TableHead>현재 도메인</TableHead>
+ <TableHead>할당일</TableHead>
+ <TableHead>설명</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {existingAssignments.map((assignment) => (
+ <TableRow key={assignment.id}>
+ <TableCell className="font-medium">
+ {assignment.departmentName}
+ </TableCell>
+ <TableCell>
+ <Badge
+ variant={assignment.assignedDomain === 'evcp' ? 'default' : 'secondary'}
+ >
+ {getDomainLabel(assignment.assignedDomain)}
+ </Badge>
+ </TableCell>
+ <TableCell className="text-sm text-muted-foreground">
+ {new Date(assignment.createdAt).toLocaleDateString('ko-KR')}
+ </TableCell>
+ <TableCell className="max-w-xs truncate text-sm">
+ {assignment.description || '-'}
+ </TableCell>
+ </TableRow>
+ ))}
+ </TableBody>
+ </Table>
+ </div>
+ )}
+
+ {hasConflicts && (
+ <div className="bg-yellow-50 border-yellow-200 border rounded-md p-3">
+ <div className="flex items-start gap-2">
+ <AlertCircle className="h-4 w-4 text-yellow-600 mt-0.5" />
+ <div className="text-sm">
+ <div className="font-medium text-yellow-800">도메인 변경 주의</div>
+ <div className="text-yellow-700">
+ 일부 부서의 기존 도메인과 다른 도메인을 할당하려고 합니다.
+ 기존 할당은 자동으로 비활성화됩니다.
+ </div>
+ </div>
+ </div>
+ </div>
+ )}
+ </div>
+ </>
+ )}
+
+ <Separator />
+
+ {/* 도메인 선택 */}
+ <div className="space-y-2">
+ <Label htmlFor="domain-select" className="text-sm font-medium">
+ 할당할 도메인 *
+ </Label>
+ <Select value={selectedDomain} onValueChange={setSelectedDomain}>
+ <SelectTrigger id="domain-select">
+ <SelectValue placeholder="도메인을 선택하세요" />
+ </SelectTrigger>
+ <SelectContent>
+ {DOMAIN_OPTIONS.map((option) => (
+ <SelectItem key={option.value} value={option.value}>
+ <div className="flex flex-col">
+ <span className="font-medium">{option.label}</span>
+ <span className="text-xs text-muted-foreground">
+ {option.description}
+ </span>
+ </div>
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+
+ {selectedDomainInfo && (
+ <div className="text-sm text-muted-foreground">
+ <Badge variant="secondary" className="mr-2">
+ {selectedDomainInfo.label}
+ </Badge>
+ {selectedDomainInfo.description}
+ </div>
+ )}
+ </div>
+
+ {/* 할당 사유/설명 */}
+ <div className="space-y-2">
+ <Label htmlFor="description" className="text-sm font-medium">
+ 할당 사유 또는 설명 (선택사항)
+ </Label>
+ <Textarea
+ id="description"
+ placeholder="예: 구매 업무 담당자들에게 procurement 도메인 할당"
+ value={description}
+ onChange={(e) => setDescription(e.target.value)}
+ rows={3}
+ maxLength={500}
+ />
+ <div className="text-xs text-muted-foreground text-right">
+ {description.length}/500
+ </div>
+ </div>
+
+ {/* 주의사항 */}
+ <div className="bg-muted/50 p-3 rounded-md">
+ <div className="text-sm text-muted-foreground">
+ <div className="font-medium mb-1">⚠️ 주의사항</div>
+ <ul className="list-disc list-inside space-y-1 text-xs">
+ <li>도메인 할당은 해당 부서 소속 사용자들의 메뉴 접근 권한에 영향을 줍니다.</li>
+ <li>기존에 다른 도메인이 할당된 부서는 새로운 도메인으로 덮어씌워집니다.</li>
+ <li>Knox 조직도 변경으로 인해 부서가 삭제된 경우, 해당 할당은 고립된 레코드가 됩니다.</li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </ScrollArea>
+ </div>
+
+ <DialogFooter className="border-t pt-4">
+ <Button
+ variant="outline"
+ onClick={() => onOpenChange(false)}
+ disabled={isSubmitting || isLoading}
+ >
+ 취소
+ </Button>
+ <Button
+ onClick={handleAssign}
+ disabled={!canSubmit}
+ >
+ {isSubmitting || isLoading ? (
+ <>
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+ 할당 중...
+ </>
+ ) : (
+ `도메인 할당 (${selectedDepartments.length}개 부서)`
+ )}
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ );
+} \ No newline at end of file